//===============================================================================
// Microsoft patterns & practices Enterprise Library Contribution
// Logging Application Block
//===============================================================================
//  LogParser - Log File Parser
//  Copyright (C) 2007  Alois Kraus
//  
//  This source code is published under the Microsoft Permissive License (Ms-PL)
//  http://www.microsoft.com/resources/sharedsource/licensingbasics/permissivelicense.mspx
//===============================================================================

using System;
using System.Globalization;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Text.RegularExpressions;
using Microsoft.Practices.EnterpriseLibrary.Logging.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using EntLibContrib.Logging.Configuration;

namespace EntLibContrib.Logging.Parser
{
	/// <summary>
	/// Read the logging configuration and build from each TextFormatter message template a corresponding regular expression which
	/// is the tokenizer for the log file or the windows event log.
	/// </summary>
	public class RegularExpressionBuilder
	{
		#region Constants
		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
		/// </summary>
		public const string TimeStampToken = "timestamp";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
		/// </summary>
		public const string PropertyToken = "property";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
		/// </summary>
		public const string DictionaryToken = "dictionary";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string MessageToken = "message";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
		/// </summary>
		public const string CategoryToken = "category";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
		/// </summary>
		public const string PriorityToken = "priority";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
		/// </summary>
		public const string EventIdToken = "eventid";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here.
		/// </summary>
		public const string SeverityToken = "severity";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string TitleToken = "title";
		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string ErrorMessagesToken = "errorMessages";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string MachineToken = "machine";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string AppDomainNameToken = "appDomain";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string ProcessIdToken = "processId";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string ProcessNameToken = "processName";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string ThreadNameToken = "threadName";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string Win32ThreadIdToken = "win32ThreadId";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string ActivityIdToken = "activity";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string NewLineToken = "{newline}";

		/// <summary>
		/// Token which is used by the TextFormatter. Since it is not public we have it to redeclare here
		/// </summary>
		public const string TabToken = "{tab}";

		/// <summary>
		/// Dictionary parameter token
		/// </summary>
		public const string DictionaryParameterToken = @"\{dictionary\(";

		/// <summary>
		/// Allowed values of TraceEventType
		/// </summary>
		private const string SeverityTokenValues = "Critical|Error|Warning|Information|Verbose|Start|Stop|Suspend|Resume|Transfer";
		private const string TimeStampParameterToken = @"\{timestamp\(";
		private const string ReflectedPropertyParameterToken = @"\{property\(";
		private const string LocalStartDelimiter = "local";
		private const string LocalStartDelimiterWithFormat = "local:";
		private const string LoggingSectionName = "loggingConfiguration";
		#endregion

		#region Fields
		private static readonly string[] StringTokens = new string[] {  TimeStampToken,
		                                        CategoryToken,
		                                        ErrorMessagesToken,
		                                        MachineToken,
		                                        AppDomainNameToken,
		                                        ProcessNameToken,
		                                        ThreadNameToken,
    		                                    ActivityIdToken, 
		                                        TitleToken,
		                                        MessageToken
                                            };
		private static readonly string[] NumericalTokens = new string[] {
		                                        PriorityToken,
		                                        EventIdToken,
		                                        ProcessIdToken,
		                                        Win32ThreadIdToken,
                                            };
		private LoggingSettings myLogSettings;  // Deserialized logging settings from config file
		private Dictionary<string, List<Regex>> myFileRegularExpressions;
		/// <summary>
		/// Map of template instance names to template string
		/// </summary>
		private Dictionary<string, string> myTemplateNameToTemplate;
		#endregion

		#region Properties
		/// <summary>
		/// This is the result of our hard regular expression building after we did
		/// parse the logging configuration and created from the TextFormatter templates the corresponding regular
		/// expression lists.
		/// Key is the event log name, or the log file name (without path). The values are a list regular expressions which
		/// represent the tokenizer for this file.
		/// </summary>
		/// <value>The file regular expressions.</value>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
		public Dictionary<string, List<Regex>> FileRegularExpressions
		{
			get { return myFileRegularExpressions; }
		}
		#endregion

		#region Construction
		/// <summary>
		/// Initializes a new instance of the <see cref="RegularExpressionBuilder"/> class which retrieves the logging
		/// configuration from the current applications App.Config file. Not really a real world scenario for
		/// an application which logs and the parses its own previously written log file but it makes testing a
		/// bit easier.
		/// </summary>
		public RegularExpressionBuilder()
			: this(new SystemConfigurationSource())
		{
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="RegularExpressionBuilder"/> class.
		/// </summary>
		/// <param name="source">The Enterprise Library configuration source which contains the logging settings.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
		public RegularExpressionBuilder(IConfigurationSource source)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			Init(source);
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="RegularExpressionBuilder"/> class.
		/// </summary>
		/// <param name="configFile">The config file containing the logging settings.</param>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
		public RegularExpressionBuilder(string configFile)
		{
			if (String.IsNullOrEmpty(configFile))
				throw new ArgumentException("configFile parameter was null or empty!");

			FileConfigurationSource source = new FileConfigurationSource(configFile);
			Init(source);
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Concatenate the complete template text with header and footer.
		/// </summary>
		/// <param name="listenerHeader">Header from the trace listener</param>
		/// <param name="formatterTemplate">Template from the formatter</param>
		/// <param name="listenerFooter">Footer from the listener</param>
		/// <returns>
		/// the complete template text with header and footer
		/// </returns>
		public static string ConcatCompleteTemplate(string listenerHeader, string formatterTemplate, string listenerFooter)
		{
			string completeTemplate = "";
			if (!string.IsNullOrEmpty(listenerHeader))
				completeTemplate += listenerHeader + "\r\n";
			completeTemplate += formatterTemplate + "\r\n";
			if (!string.IsNullOrEmpty(listenerFooter))
				completeTemplate += listenerFooter;
			return completeTemplate;
		}

		/// <summary>
		/// Add for this log file a template to the internal list. This function does create from the given message
		/// template the regular expression which can be used later.
		/// </summary>
		/// <param name="fileName">Name of the log file the template was used for.</param>
		/// <param name="messageTemplate">The message template.</param>
		public virtual void AddFileNameAndTemplate(string fileName, string messageTemplate)
		{
			if (!myFileRegularExpressions.ContainsKey(fileName))
			{
				List<Regex> list = new List<Regex>();
				list.Add(BuildRegularExpressionFromTemplate(messageTemplate));
				myFileRegularExpressions.Add(fileName, list);
			}
			else
			{
				List<Regex> list = myFileRegularExpressions[fileName];
				list.Add(BuildRegularExpressionFromTemplate(messageTemplate));
			}
		}

		/// <summary>
		/// Builds the regular expression from a message template.
		/// </summary>
		/// <param name="messageTemplate">The message template.</param>
		/// <returns>
		/// A regular expression which can match every message which was written with this template
		/// </returns>
		public static Regex DoBuildRegularExpressionFromTemplate(string messageTemplate)
		{
			StringBuilder regex = new StringBuilder(messageTemplate);
			regex.Replace(NewLineToken, Environment.NewLine);
			regex.Replace(TabToken, "\t");

			// escape the template so no other tokens are recognized as regex sequence later
			regex = new StringBuilder(Regex.Escape(regex.ToString()));

			// numerical tokens are treated different to allow faster and better performing matching
			foreach (string numToken in NumericalTokens)
			{
				regex.Replace(EscapeToken(numToken), String.Format(CultureInfo.CurrentCulture, "(?<{0}>[+-]?\\d+)", numToken));
			}

			// Each string token is as greedy it should be
			foreach (string stringToken in StringTokens)
			{
				regex.Replace(EscapeToken(stringToken), String.Format(CultureInfo.CurrentCulture, "(?<{0}>.*?)", stringToken));
			}

			// Severity is special because its value is only a fixed set of values.
			regex.Replace(EscapeToken(SeverityToken), String.Format(CultureInfo.CurrentCulture, "(?<{0}>{1})", SeverityToken, SeverityTokenValues));

			// The dictionary(....) token is treated as blob
			string curRegex = TreatDictionary(regex.ToString());

			curRegex = TreatToken(curRegex, TimeStampParameterToken, TimeStampToken);
			curRegex = TreatToken(curRegex, ReflectedPropertyParameterToken, PropertyToken);

			// Finally we can create the regular expression which will match every single message from a log file or the windows
			// event log
			return new Regex(curRegex, RegexOptions.Singleline);
		}
		#endregion

		#region Protected Methods
		/// <summary>
		/// Builds the template instance list. It does iterate over all text formatters which have been read
		/// from the logging configuration and creates a regular expression from every message template. Each
		/// process template is stored in a dictionar with its name.
		/// </summary>
		protected virtual void BuildTemplateInstanceList()
		{
			foreach (FormatterData formatterData in myLogSettings.Formatters)
			{
				ITemplateTextFormatterData textFormatter = CategoryLogFileReader.GetTextFormatterData(formatterData);
				if (textFormatter != null)
				{
					myTemplateNameToTemplate.Add(textFormatter.Name, textFormatter.Template);
					//Console.WriteLine("Template: " + textFormatter.Template);
				}
			}
		}

		/// <summary>
		/// Create the final message template strings. At first we get from the FileListeners the header and footer strings which are added to the
		/// complete template. Then we create for each configured log file from the full template string the regular expression which can parse
		/// every message written into the log file or event logs.
		/// </summary>
		protected virtual void CreateRegexFromListenerConfig()
		{
			foreach (TraceListenerData tracedata in myLogSettings.TraceListeners)
			{
				FlatFileTraceListenerData filedata = tracedata as FlatFileTraceListenerData;
				RollingFlatFileTraceListenerData rollingfiledata = tracedata as RollingFlatFileTraceListenerData;
				FormattedEventLogTraceListenerData eventlogData = tracedata as FormattedEventLogTraceListenerData;

				if (filedata != null)
				{
					string completeTemplate = ConcatCompleteTemplate(
							filedata.Header,
							myTemplateNameToTemplate[filedata.Formatter],
							filedata.Footer);
					AddFileNameAndTemplate(Path.GetFileName(filedata.FileName), completeTemplate);
				}
				else if (rollingfiledata != null)
				{
					string completeTemplate = ConcatCompleteTemplate(
							rollingfiledata.Header,
							myTemplateNameToTemplate[rollingfiledata.Formatter],
							rollingfiledata.Footer);
					AddFileNameAndTemplate(Path.GetFileName(rollingfiledata.FileName), completeTemplate);
				}
				else if (eventlogData != null)
				{
					AddFileNameAndTemplate(eventlogData.Log, myTemplateNameToTemplate[eventlogData.Formatter]);
				}
				else
				{
					Debug.Print("Ignored trace listener {0}, type {1} because it was no file or event log listener with a formatter", tracedata.Name, tracedata.Type);
				}
			}
		}

		/// <summary>
		/// Escapes the token name with its enclosing tags plus the regular expression escape char for the { char
		/// </summary>
		/// <param name="tokenName">The token name.</param>
		/// <returns>Escaped token</returns>
		protected static string EscapeToken(string tokenName)
		{
			if (String.IsNullOrEmpty(tokenName))
				Debug.Assert(false, "string tokenName is null or empty!");

			return "\\{" + tokenName + "}";
		}

		/// <summary>
		/// Treat the special timeStamp( and property( tokens as blobs to create a correct regex from it.
		/// </summary>
		/// <param name="regex">The current regular expresson message template string.</param>
		/// <param name="token">The token name.</param>
		/// <param name="varName">Name of the capturing group which will match the formatted token string.</param>
		/// <returns>
		/// Changed regular expression message template
		/// </returns>
		protected static string TreatToken(string regex, string token, string varName)
		{
			int start = regex.IndexOf(token, StringComparison.CurrentCulture);
			if (start != -1)
			{
				int stop = regex.IndexOf('}', start);
				Debug.Assert(stop != -1, String.Format(CultureInfo.CurrentCulture, "Could not find stop token in {0}", token));
				if (stop != -1)
				{
					return regex.Remove(start, stop-start+1).Insert(start, "(?<" + varName + ">.*?)");
				}
			}

			return regex;
		}

		/// <summary>
		/// Treat the dictionary token as blob data which will be stored as one string in the dictionary
		/// </summary>
		/// <param name="regex">Current Regular expression string</param>
		/// <returns>Replaced dictionary token if present.</returns>
		protected static string TreatDictionary(string regex)
		{
			// Check if escaped Dictionary token is present "\{dictionary\("
			int startOfToken = regex.IndexOf(DictionaryParameterToken, StringComparison.CurrentCulture);
			if (startOfToken != -1)
			{
				int openCount = 0;
				int endOfToken = 0;
				bool wasGreaterZero = false;

				// search for the end of the dictionary
				for (int i = startOfToken; i < regex.Length; i++)
				{
					if (regex[i] == '{')
					{
						openCount++;
						wasGreaterZero = true;
					}
					else if (regex[i] == '}')
						openCount--;

					if (openCount == 0 && wasGreaterZero)
					{
						endOfToken = i;
						break;
					}
				}

				// when opencount is not zero the template was garbage.
				if (openCount == 0)
				{
					// remove the dictionary formatting and treat all formatted key/value pairs as one blob
					return regex.Remove(startOfToken, endOfToken - startOfToken + 1).Insert(startOfToken, "(?<" + DictionaryToken + ">.*?)");
				}
				else
				{
					Debug.Assert(false, "Dictionary start was found but no end could be determined");
				}
			}

			return regex;
		}

		/// <summary>
		/// Builds the regular expression from a message template.
		/// </summary>
		/// <param name="messageTemplate">The message template.</param>
		/// <returns>
		/// A regular expression which can match every message which was written with this template
		/// </returns>
		protected virtual Regex BuildRegularExpressionFromTemplate(string messageTemplate)
		{
			return DoBuildRegularExpressionFromTemplate(messageTemplate);
		}
		#endregion

		#region Private Methods
		private void Init(IConfigurationSource source)
		{
			myFileRegularExpressions = new Dictionary<string, List<Regex>>();
			myTemplateNameToTemplate = new Dictionary<string, string>();

			myLogSettings = source.GetSection(LoggingSectionName) as LoggingSettings;
			if (myLogSettings == null)
				throw new InvalidOperationException("Could not read logging settings from configuration!");

			BuildTemplateInstanceList();
			CreateRegexFromListenerConfig();
		}
		#endregion
	}
}
